Prior to .NET, error handling under the Windows operating system was a confused mishmash of techniques. Many programmers rolled their own error-handling logic within the context of a given application. For example, a development team could define a set of numerical constants that represented known error conditions, and make use of them as method return values. By way of an example, consider the following partial C code:
/* A very C-style error trapping mechanism. */ #define E_FILENOTFOUND 1000 int SomeFunction() { // Assume something happens in this function // that causes the following return value. return E_FILENOTFOUND; } void main() { int retVal = SomeFunction(); if(retVal == E_FILENOTFOUND) printf("Cannot find file..."); }
This approach is less than ideal, given the fact that the constant E_FILENOTFOUND is little more than a numerical value, and is far from being a helpful agent regarding how to deal with the problem. Ideally, you would like to wrap the error’s name, a descriptive message, and other helpful information about this error condition into a single, well-defined package (which is exactly what happens under structured exception handling).
In addition to a developer’s ad hoc techniques, the Windows API defines hundreds of error codes that come by way of #defines, HRESULTs, and far too many variations on the simple Boolean (bool, BOOL, VARIANT_BOOL, and so on). Furthermore, many C++ COM developers (and indirectly, many VB6 COM developers) made use of a small set of standard COM interfaces (e.g., ISupportErrorInfo, IErrorInfo, ICreateErrorInfo) to return meaningful error information to a COM client.
The obvious problem with these older techniques is the tremendous lack of symmetry. Each approach is more or less tailored to a given technology, a given language, and perhaps even a given project. To put an end to this madness, the .NET platform provides a standard technique to send and trap runtime errors: structured exception handling (SEH).
The beauty of this approach is that developers now have a unified approach to error handling, which is common to all languages targeting the .NET platform. Therefore, the way in which a C# programmer handles errors is syntactically similar to that of a VB programmer, or a C++ programmer using C++/CLI.
As an added bonus, the syntax used to throw and catch exceptions across assemblies and machine boundaries is identical. For example, if you use C# to build a Windows Communication Foundation (WCF) service, you can throw a SOAP fault to a remote caller, using the same keywords that allow you to throw an exception between methods in the same application.
Another bonus of .NET exceptions is that rather than receiving a cryptic numerical value that simply identifies the problem at hand, exceptions are objects that contain a human-readable description of the problem, as well as a detailed snapshot of the call stack that triggered the exception in the first place. Furthermore, you are able to give the end user help-link information that points the user to a URL that provides details about the error, as well as custom programmer-defined data.
Programming with structured exception handling involves the use of four interrelated entities:
A block of code on the caller’s side that will process (or catch) the exception should it occur
The C# programming language offers four keywords (try, catch, throw, and finally) that allow you to throw and handle exceptions. The object that represents the problem at hand is a class extending System.Exception (or a descendent thereof). Given this fact, let’s check out the role of this exceptioncentric base class.
All user- and system-defined exceptions ultimately derive from the System.Exception base class, which in turn derives from System.Object. Here is the crux of this class (note that some of these members are virtual and may thus be overridden by derived classes):
public class Exception : ISerializable, _Exception { // Public constructors public Exception(string message, Exception innerException); public Exception(string message); public Exception(); ... // Methods public virtual Exception GetBaseException(); public virtual void GetObjectData(SerializationInfo info, StreamingContext context); // Properties public virtual IDictionary Data { get; } public virtual string HelpLink { get; set; } public Exception InnerException { get; } public virtual string Message { get; } public virtual string Source { get; set; } public virtual string StackTrace { get; } public MethodBase TargetSite { get; } ... }
As you can see, many of the properties defined by System.Exception are read-only in nature. This is due to the fact that derived types will typically supply default values for each property. For example, the default message of the IndexOutOfRangeException type is “Index was outside the bounds of the array.”
Note The Exception class implements two .NET interfaces. Although we have yet to examine interfaces (see Chapter 9), just understand that the _Exception interface allows a .NET exception to be processed by an unmanaged code base (such as a COM application), while the ISerializable interface allows an exception object to be persisted across boundaries (such as a machine boundary).
Table 7-1 describes the most important members of System.Exception.
Table 7-1. Core Members of the System.Exception Type
System.Exception Property | Meaning in Life |
---|---|
Data | This read-only property retrieves a collection of key/value pairs (represented by an object implementing IDictionary) that provide additional, programmer-defined information about the exception. By default, this collection is empty. |
InnerException | This read-only property can be used to obtain information about the previous exception(s) that caused the current exception to occur. The previous exception(s) are recorded by passing them into the constructor of the most current exception. |
Message | This read-only property returns the textual description of a given error. The error message itself is set as a constructor parameter. |
Source | This property gets or sets the name of the assembly, or the object, that threw the current exception. |
StackTrace | This read-only property contains a string that identifies the sequence of calls that triggered the exception. As you might guess, this property is very useful during debugging or if you wish to dump the error to an external error log. |
TargetSite | This read-only property returns a MethodBase object, which describes numerous details about the method that threw the exception (invoking ToString() will identify the method by name). |